package com.googlecode.yoohoo.xep.register;

import java.util.Locale;

import org.osgi.framework.BundleContext;

import com.googlecode.yoohoo.message.IMessageManager;
import com.googlecode.yoohoo.utils.AppUtils;
import com.googlecode.yoohoo.utils.OsgiUtils;
import com.googlecode.yoohoo.xep.data.protocol.Data;
import com.googlecode.yoohoo.xep.data.protocol.Field;
import com.googlecode.yoohoo.xep.data.protocol.FieldType;
import com.googlecode.yoohoo.xep.register.protocol.Register;
import com.googlecode.yoohoo.xmppcore.connection.IConnectionContext;
import com.googlecode.yoohoo.xmppcore.model.IXmppCoreModelFactory;
import com.googlecode.yoohoo.xmppcore.model.account.AccountConflictException;
import com.googlecode.yoohoo.xmppcore.model.account.IAccountManager;
import com.googlecode.yoohoo.xmppcore.model.account.InvalidAccountException;
import com.googlecode.yoohoo.xmppcore.model.orm.account.Account;
import com.googlecode.yoohoo.xmppcore.model.orm.account.Gender;
import com.googlecode.yoohoo.xmppcore.protocol.IError;
import com.googlecode.yoohoo.xmppcore.protocol.XmppProtocolException;
import com.googlecode.yoohoo.xmppcore.protocol.stanza.Iq;
import com.googlecode.yoohoo.xmppcore.protocol.stanza.IqType;
import com.googlecode.yoohoo.xmppcore.protocol.stanza.error.SimpleApplicationSpecificCondition;
import com.googlecode.yoohoo.xmppcore.protocol.stanza.error.BadRequest;
import com.googlecode.yoohoo.xmppcore.protocol.stanza.error.ErrorType;
import com.googlecode.yoohoo.xmppcore.protocol.stanza.error.UndefinedCondition;
import com.googlecode.yoohoo.xmppcore.protocol.stream.error.Conflict;
import com.googlecode.yoohoo.xmppcore.stanza.IStanzaHandler;
import com.googlecode.yoohoo.xmppcore.stanza.IStanzaHandlerFactory;
import com.googlecode.yoohoo.xmppcore.stanza.IStanzaTypeMatcher;
import com.googlecode.yoohoo.xmppcore.stanza.StanzaHandlerAdapter;
import com.googlecode.yoohoo.xmppcore.stanza.StanzaIdKey;
import com.googlecode.yoohoo.xmppcore.utils.ErrorUtils;

public class RegisterHandlerFactory<V extends IConnectionContext> implements
		IStanzaHandlerFactory<V, Iq>, IStanzaTypeMatcher<Iq> {
	private BundleContext bundleContext;
	
	public RegisterHandlerFactory(BundleContext bundleContext) {
		this.bundleContext = bundleContext;
	}

	@Override
	public IStanzaHandler<V, Iq> createHandler() {
		return new RegisterHandler<V>(bundleContext);
	}
	
	private class RegisterHandler<K extends IConnectionContext> extends StanzaHandlerAdapter<K, Iq> {
		private static final String REGISTER_FORM_TEMPLATE =
			"<iq type='result' id='%s' from='%s' xml:lang='%s'>" +
				"<query xmlns='jabber:iq:register'>" +
					"<x xmlns='jabber:x:data' type='form'>" +
						"<title>%s</title>" +
						"<instructions>%s</instructions>" +
						"<field type='hidden' var='FORM_TYPE'>" +
							"<value>jabber:iq:data</value>" +
						"</field>" +
						"<field type='text-single' label='%s' var='username'>" +
							"<required/>" +
						"</field>" +
						"<field type='text-private' label='%s' var='password'>" +
							"<desc>%s</desc>" +
							"<required/>" +
						"</field>" +
						"<field type='text-private' label='%s' var='confirmedpassword'>" +
							"<required/>" +
						"</field>" +
						"<field type='text-single' label='%s' var='realname'/>" +
						"<field type='list-single' label='%s' var='x-gender'>" +
							"<option label='%s'><value>M</value></option>" +
							"<option label='%s'><value>F</value></option>" +
						"</field>" +
					"</x>" +
				"</query>" +
			"</iq>";

		private boolean formReturned;
		private BundleContext bundleContext;
		
		public RegisterHandler(BundleContext bundleContext) {
			formReturned = false;
			this.bundleContext = bundleContext;
		}
		
		@Override
		public boolean process(K context, Iq iq) {
			if (!formReturned) {
				processFormRequest(context, iq);			
				return false;
			} else {
				processRegister(context, iq);
				return true;
			}
		}
		
		@Override
		public void restoreStates(K context, Iq iq) {
			RegisterFormReturnedKey key = new RegisterFormReturnedKey(iq.getId());
			if (context.getAttribute(key) != null)
				formReturned = true;
		}
		
		@Override
		public void saveStates(K context, Iq iq) {
			RegisterFormReturnedKey key = new RegisterFormReturnedKey(iq.getId());
			context.setAttribute(key, true);
		}
		
		@Override
		public void complete(K context, Iq iq) {
			context.removeAttribute(new RegisterFormReturnedKey(iq.getId()));
		}
		
		private void processRegister(K context, Iq iq) {
			Register register = (Register)iq.getObject();
			Data data = register.getData();
			
			IXmppCoreModelFactory modelFactory = OsgiUtils.getService(bundleContext, IXmppCoreModelFactory.class);
			Account user = (Account)modelFactory.createAccount();
			String confirmedPassword = null;
			for (Field field : data.getFields()) {
				if ("FORM_TYPE".equals(field.getVar())) {
					if (field.getType() != FieldType.HIDDEN)
						throw ErrorUtils.createProtocolException("Type of field 'FORM_TYPE' must be 'hidden'",
								new BadRequest());
					if (!field.getValues().get(0).equals("jabber:iq:register")) {
						throw ErrorUtils.createProtocolException("Value of field 'FORM_TYPE' must be 'jabber:iq:register'",
								new BadRequest());
					}
				} else if ("username".equals(field.getVar())) {
					user.setName(field.getValues().size() == 0 ? null : field.getValues().get(0));
				} else if ("password".equals(field.getVar())) {
					user.setPassword(field.getValues().size() == 0 ? null : field.getValues().get(0));
				} else if ("confirmedpassword".equals(field.getVar())) {
					confirmedPassword = field.getValues().size() == 0 ? null : field.getValues().get(0);
				} else if ("realname".equals(field.getVar())) {
					user.setRealName(field.getValues().size() == 0 ? null : field.getValues().get(0));
				} else if ("x-gender".equals(field.getVar())) {
					String gender = field.getValues().size() == 0 ? null : field.getValues().get(0);
						
					if ("M".equals(gender)) {
						user.setGender(Gender.MALE);
					} else if ("F".equals(gender)) {
						user.setGender(Gender.FEMALE);
					} else {
						throw ErrorUtils.createProtocolException(String.format("Invalid x-gender value [%s]",
								gender), new BadRequest());
					}
				} else {
					throw ErrorUtils.createProtocolException(String.format("Unknown field [%s]",
							field.getVar()), new BadRequest());
				}
			}
			
			IMessageManager messageManager = AppUtils.getMessageManager(bundleContext);
			if (user.getName() == null) {
				UndefinedCondition error = new UndefinedCondition(ErrorType.MODIFY);
				error.setApplicationSpecificCondition(
						new SimpleApplicationSpecificCondition(
						"null-user-name",
						"urn:yoohoo:ns:in-band-register",
						messageManager.getMessage("registerhandler.error.desc.usernameisnull",
								AppUtils.xmlLangToLocale(iq.getXmlLang())),
						iq.getXmlLang()));
				throw new XmppProtocolException(error);
			}
			
			if (user.getPassword() == null) {
				UndefinedCondition error = new UndefinedCondition(ErrorType.MODIFY);
				error.setApplicationSpecificCondition(
						new SimpleApplicationSpecificCondition(
						"null-password",
						"urn:yoohoo:ns:in-band-register",
						messageManager.getMessage("registerhandler.error.desc.passwordisnull",
								AppUtils.xmlLangToLocale(iq.getXmlLang())),
						iq.getXmlLang()));
				throw new XmppProtocolException(error);
			}
			
			if (confirmedPassword == null) {
				UndefinedCondition error = new UndefinedCondition(ErrorType.MODIFY);
				error.setApplicationSpecificCondition(
						new SimpleApplicationSpecificCondition(
						"null-confirmed-password",
						"urn:yoohoo:ns:in-band-register",
						messageManager.getMessage("registerhandler.error.desc.confirmedpasswordisnull",
								AppUtils.xmlLangToLocale(iq.getXmlLang())),
						iq.getXmlLang()));
				throw new XmppProtocolException(error);
			}
			
			if (!confirmedPassword.equals(user.getPassword())) {
				UndefinedCondition error = new UndefinedCondition(ErrorType.MODIFY);
				error.setApplicationSpecificCondition(
						new SimpleApplicationSpecificCondition(
						"passwords-not-equal",
						"urn:yoohoo:ns:in-band-register",
						messageManager.getMessage("registerhandler.error.desc.passwordsnotequal",
								AppUtils.xmlLangToLocale(iq.getXmlLang())),
						iq.getXmlLang()));
				throw new XmppProtocolException(error);
			}
			
			if (user.getPassword().length() < 6) {
				UndefinedCondition error = new UndefinedCondition(ErrorType.MODIFY);
				error.setApplicationSpecificCondition(
						new SimpleApplicationSpecificCondition(
						"password-length-too-short",
						"urn:yoohoo:ns:in-band-register",
						messageManager.getMessage("registerhandler.error.desc.passwordlength",
								AppUtils.xmlLangToLocale(iq.getXmlLang())),
						iq.getXmlLang()));
				throw new XmppProtocolException(error);
			}
			
			IAccountManager accountManager = (IAccountManager)OsgiUtils.getService(bundleContext, IAccountManager.class);		
			
			try {
				accountManager.create(user);
			} catch (AccountConflictException e) {
				throw ErrorUtils.createProtocolException("User name has aleady existed.", new Conflict());
			} catch (InvalidAccountException e) {
				throw ErrorUtils.createProtocolException("Invalid user name.", new Conflict());
			}
			
			
			Iq result = new Iq();
			result.setTo(iq.getFrom());
			result.setType("result");
			result.setId(iq.getId());
			
			context.write(result.toXml(), true);
			context.close();
		}
		
		@Override
		public void exceptionThrew(K context, Iq iq, XmppProtocolException e) {
			IError error = e.getError();
			boolean saveState = false;
			if ((error instanceof SimpleApplicationSpecificCondition) ||
					(error instanceof Conflict) ||
					(error instanceof UndefinedCondition)) {
				saveState = true;
			}
			
			if (saveState) {
				saveStates(context, iq);
			}
		}

		private void processFormRequest(K context, Iq iq) {
			if (!IqType.GET.toString().equals(iq.getType())) {
				throw ErrorUtils.createProtocolException("Register from request must be a 'get' type stanza.", new BadRequest());
			}

			IMessageManager messageManager = AppUtils.getMessageManager(bundleContext);
			Locale locale = AppUtils.xmlLangToLocale(iq.getXmlLang());
			String response = String.format(
				REGISTER_FORM_TEMPLATE,
				iq.getId(),
				AppUtils.getHostName(bundleContext),
				iq.getXmlLang(),
				messageManager.getMessage("registerhandler.registerform.template.form.title", locale),
				messageManager.getMessage("registerhandler.registerform.template.form.instructions", locale),
				messageManager.getMessage("registerhandler.registerform.template.form.username.label", locale),
				messageManager.getMessage("registerhandler.registerform.template.form.password.label", locale),
				messageManager.getMessage("registerhandler.registerform.template.form.password.desc", locale),
				messageManager.getMessage("registerhandler.registerform.template.form.confirmedpassword.label", locale),
				messageManager.getMessage("registerhandler.registerform.template.form.realname.label", locale),
				messageManager.getMessage("registerhandler.registerform.template.form.x-gender.label", locale),
				messageManager.getMessage("registerhandler.registerform.template.form.x-gender.male.label", locale),
				messageManager.getMessage("registerhandler.registerform.template.form.x-gender.female.label", locale)
			);

			context.write(response);
		}
	}

	@Override
	public boolean match(Class<Iq> stanzaClass, Class<?>... objectClasses) {
		if (objectClasses.length != 1) {
			return false;
		}
		
		return objectClasses[0].equals(Register.class);
	}

	private class RegisterFormReturnedKey extends StanzaIdKey {
		public RegisterFormReturnedKey(String id) {
			super(id);
		}
	}
	
}
